/*
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.yixia.zi.utils;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.widget.ImageView;

import com.yixia.zi.BuildConfig;

/**
 * A subclass of {@link ImageWorker} that fetches images from a URL.
 */
public class ImageFetcher extends ImageWorker {
	private static final String TAG = "ImageFetcher";

	public static final int IO_BUFFER_SIZE_BYTES = 4 * 1024; // 4KB

	private ImageFetcherParams mFetcherParams;

	// Default fetcher params
	private static final int DEFAULT_MAX_THUMBNAIL_BYTES = 70 * 1024; // 70KB
	private static final int DEFAULT_MAX_IMAGE_HEIGHT = 1024;
	private static final int DEFAULT_MAX_IMAGE_WIDTH = 1024;
	private static final int DEFAULT_MIN_IMAGE_HEIGHT = 256;
	private static final int DEFAULT_MIN_IMAGE_WIDTH = 256;
	private static final int DEFAULT_HTTP_CACHE_SIZE = 5 * 1024 * 1024; // 5MB
	private static final String DEFAULT_HTTP_CACHE_DIR = "http";

	/**
	 * Create an ImageFetcher specifying custom parameters.
	 */
	public ImageFetcher(Context context, ImageFetcherParams params) {
		super(context);
		setParams(params);
	}

	/**
	 * Create an ImageFetcher using default parameters.
	 */
	public ImageFetcher(Context context) {
		super(context);
		setParams(new ImageFetcherParams());
	}

	public void loadThumbnailImage(String key, ImageView imageView, Bitmap loadingBitmap) {
		loadImage(new ImageData(key, ImageData.IMAGE_TYPE_THUMBNAIL), imageView, loadingBitmap);
	}

	public void loadThumbnailImage(String key, ImageView imageView, int resId) {
		loadImage(new ImageData(key, ImageData.IMAGE_TYPE_THUMBNAIL), imageView, resId);
	}

	public void loadThumbnailImage(String key, ImageView imageView) {
		loadImage(new ImageData(key, ImageData.IMAGE_TYPE_THUMBNAIL), imageView, mLoadingBitmap);
	}

	public void loadImage(String key, ImageView imageView, Bitmap loadingBitmap) {
		loadImage(new ImageData(key, ImageData.IMAGE_TYPE_NORMAL), imageView, loadingBitmap);
	}

	public void loadImage(String key, ImageView imageView, int resId) {
		loadImage(new ImageData(key, ImageData.IMAGE_TYPE_NORMAL), imageView, resId);
	}

	public void loadImage(String key, ImageView imageView) {
		loadImage(new ImageData(key, ImageData.IMAGE_TYPE_NORMAL), imageView, mLoadingBitmap);
	}

	public void loadLocalImage(String key, ImageView imageView) {
		loadImage(new ImageData(key, ImageData.IMAGE_TYPE_LOCAL), imageView, mLoadingBitmap);
	}

	public void setParams(ImageFetcherParams params) {
		mFetcherParams = params;
	}

	/**
	 * Set the target image width and height.
	 */
	public void setImageSize(int width, int height) {
		mFetcherParams.mImageWidth = width;
		mFetcherParams.mImageHeight = height;
	}

	/**
	 * Set the target image size (width and height will be the same).
	 */
	public void setImageSize(int size) {
		setImageSize(size, size);
	}

	/**
	 * The main process method, which will be called by the ImageWorker in the
	 * AsyncTask background thread.
	 * 
	 * @param key The key to load the bitmap, in this case, a regular http URL
	 * @return The downloaded and resized bitmap
	 */
	private Bitmap processBitmap(String key, int type) {
		Log.d(TAG, "processBitmap - " + key);

		if (type == ImageData.IMAGE_TYPE_NORMAL) {
			final File f = downloadBitmapToFile(mContext, key, mFetcherParams.mHttpCacheDir);
			if (f != null) {
				// Return a sampled down version
				final Bitmap bitmap = decodeSampledBitmapFromFile(f.toString(), mFetcherParams.mImageWidth, mFetcherParams.mImageHeight);
				f.delete();
				return bitmap;
			}
		} else if (type == ImageData.IMAGE_TYPE_THUMBNAIL) {

			final byte[] bitmapBytes = downloadBitmapToMemory(mContext, key, mFetcherParams.mMaxThumbnailBytes);

			if (bitmapBytes != null) {
				// Caution: we don't check the size of the bitmap here, we are relying on the output
				// of downloadBitmapToMemory to not exceed our memory limits and load a huge bitmap
				// into memory.
				return BitmapFactory.decodeByteArray(bitmapBytes, 0, bitmapBytes.length);
			}
		} else if (type == ImageData.IMAGE_TYPE_LOCAL) {
			// Return a sampled down version
			final Bitmap bitmap = decodeSampledBitmapFromFile(key, mFetcherParams.mImageThumbnailWidth, mFetcherParams.mImageThumbnailHeight);
			return bitmap;
		}
		return null;
	}

	@Override
	protected Bitmap processBitmap(Object key) {
		final ImageData imageData = (ImageData) key;
		return processBitmap(imageData.mKey, imageData.mType);
	}

	/**
	 * Download a bitmap from a URL, write it to a disk and return the File
	 * pointer. This implementation uses a simple disk cache.
	 * 
	 * @param context The context to use
	 * @param urlString The URL to fetch
	 * @param maxBytes The maximum number of bytes to read before returning null
	 * to protect against OutOfMemory exceptions.
	 * @return A File pointing to the fetched bitmap
	 */
	public static byte[] downloadBitmapToMemory(Context context, String urlString, int maxBytes) {
		HttpURLConnection urlConnection = null;
		ByteArrayOutputStream out = null;
		InputStream in = null;

		try {
			final URL url = new URL(urlString);
			urlConnection = (HttpURLConnection) url.openConnection();
			if (urlConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
				return null;
			}
			in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE_BYTES);
			out = new ByteArrayOutputStream(IO_BUFFER_SIZE_BYTES);

			final byte[] buffer = new byte[128];
			int total = 0;
			int bytesRead;
			while ((bytesRead = in.read(buffer)) != -1) {
				total += bytesRead;
				if (total > maxBytes) {
					return null;
				}
				out.write(buffer, 0, bytesRead);
			}
			return out.toByteArray();

		} catch (final IOException e) {
			Log.e(TAG, "Error in downloadBitmapToMemory - " + e);
		} finally {
			if (urlConnection != null) {
				urlConnection.disconnect();
			}
			try {
				if (out != null) {
					out.close();
				}
				if (in != null) {
					in.close();
				}
			} catch (final IOException e) {
				Log.e(TAG, "Error in downloadBitmapToMemory - " + e);
			}
		}
		return null;
	}

	/**
	 * Download a bitmap from a URL, write it to a disk and return the File
	 * pointer. This implementation uses a simple disk cache.
	 * 
	 * @param context The context to use
	 * @param urlString The URL to fetch
	 * @return A File pointing to the fetched bitmap
	 */
	public static File downloadBitmapToFile(Context context, String urlString, String uniqueName) {
		final File cacheDir = ImageCache.getDiskCacheDir(context, uniqueName);

		if (!cacheDir.exists()) {
			cacheDir.mkdir();
		}
		
		if (BuildConfig.DEBUG) {
			Log.d(TAG, "downloadBitmap - downloading - " + urlString);
		}

		HttpURLConnection urlConnection = null;
		BufferedOutputStream out = null;

		try {
			final File tempFile = File.createTempFile("bitmap", null, cacheDir);

			final URL url = new URL(urlString);
			urlConnection = (HttpURLConnection) url.openConnection();
			if (urlConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
				return null;
			}
			final InputStream in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE_BYTES);
			out = new BufferedOutputStream(new FileOutputStream(tempFile), IO_BUFFER_SIZE_BYTES);

			int b;
			while ((b = in.read()) != -1) {
				out.write(b);
			}

			return tempFile;

		} catch (final IOException e) {
			Log.e(TAG, "Error in downloadBitmap - " + e);
		} finally {
			if (urlConnection != null) {
				urlConnection.disconnect();
			}
			if (out != null) {
				try {
					out.close();
				} catch (final IOException e) {
					Log.e(TAG, "Error in downloadBitmap - " + e);
				}
			}
		}

		return null;
	}

	/**
	 * Decode and sample down a bitmap from a file to the requested width and
	 * height.
	 * 
	 * @param filename The full path of the file to decode
	 * @param reqWidth The requested width of the resulting bitmap
	 * @param reqHeight The requested height of the resulting bitmap
	 * @return A bitmap sampled down from the original with the same aspect ratio
	 * and dimensions that are equal to or greater than the requested width and
	 * height
	 */
	public static synchronized Bitmap decodeSampledBitmapFromFile(String filename, int reqWidth, int reqHeight) {

		// First decode with inJustDecodeBounds=true to check dimensions
		final BitmapFactory.Options options = new BitmapFactory.Options();
		options.inJustDecodeBounds = true;
		BitmapFactory.decodeFile(filename, options);

		// Calculate inSampleSize
		options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

		// Decode bitmap with inSampleSize set
		options.inJustDecodeBounds = false;
		return BitmapFactory.decodeFile(filename, options);
	}

	/**
	 * Calculate an inSampleSize for use in a
	 * {@link android.graphics.BitmapFactory.Options} object when decoding bitmaps
	 * using the decode* methods from {@link BitmapFactory}. This implementation
	 * calculates the closest inSampleSize that will result in the final decoded
	 * bitmap having a width and height equal to or larger than the requested
	 * width and height. This implementation does not ensure a power of 2 is
	 * returned for inSampleSize which can be faster when decoding but results in
	 * a larger bitmap which isn't as useful for caching purposes.
	 * 
	 * @param options An options object with out* params already populated (run
	 * through a decode* method with inJustDecodeBounds==true
	 * @param reqWidth The requested width of the resulting bitmap
	 * @param reqHeight The requested height of the resulting bitmap
	 * @return The value to be used for inSampleSize
	 */
	public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
		// Raw height and width of image
		final int height = options.outHeight;
		final int width = options.outWidth;
		int inSampleSize = 1;

		if (height > reqHeight || width > reqWidth) {
			if (width > height) {
				inSampleSize = Math.round((float) height / (float) reqHeight);
			} else {
				inSampleSize = Math.round((float) width / (float) reqWidth);
			}

			// This offers some additional logic in case the image has a strange
			// aspect ratio. For example, a panorama may have a much larger
			// width than height. In these cases the total pixels might still
			// end up being too large to fit comfortably in memory, so we should
			// be more aggressive with sample down the image (=larger
			// inSampleSize).

			final float totalPixels = width * height;

			// Anything more than 2x the requested pixels we'll sample down
			// further.
			final float totalReqPixelsCap = reqWidth * reqHeight * 2;

			while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
				inSampleSize++;
			}
		}
		return inSampleSize;
	}

	public static class ImageFetcherParams {
		public int mImageWidth = DEFAULT_MAX_IMAGE_WIDTH;
		public int mImageHeight = DEFAULT_MAX_IMAGE_HEIGHT;
		public int mMaxThumbnailBytes = DEFAULT_MAX_THUMBNAIL_BYTES;
		public int mHttpCacheSize = DEFAULT_HTTP_CACHE_SIZE;
		public String mHttpCacheDir = DEFAULT_HTTP_CACHE_DIR;
		public int mImageThumbnailWidth = DEFAULT_MIN_IMAGE_WIDTH;
		public int mImageThumbnailHeight = DEFAULT_MIN_IMAGE_HEIGHT;
	}

	private static class ImageData {
		public static final int IMAGE_TYPE_THUMBNAIL = 0;
		public static final int IMAGE_TYPE_NORMAL = 1;
		public static final int IMAGE_TYPE_LOCAL = 2;
		public String mKey;
		public int mType;

		public ImageData(String key, int type) {
			mKey = key;
			mType = type;
		}

		@Override
		public String toString() {
			return mKey;
		}
	}
}
